package pt.compflow.activity.workflow;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import pt.compflow.Context;
import pt.compflow.activity.ActivityControllerListener;
import pt.compflow.flowontology.model.Activity;
import pt.compflow.flowontology.model.Event;
import pt.compflow.flowontology.model.FLOWO;
import pt.compflow.flowontology.model.FlowOntologyModel;
import pt.compflow.flowontology.model.Job;
import pt.compflow.flowontology.model.Loop;
import pt.compflow.flowontology.model.Workflow;
import pt.compflow.graph.Tree;
import pt.compflow.interfaces.event.EventSubscription;
import pt.compflow.storage.TripleStoreConnection;
import pt.compflow.tools.OntModelHelper;
import pt.ipp.isep.gecad.o2c.model.impl.DateContainer;
import pt.ipp.isep.gecad.o2c.model.impl.DateContainer.DateContainerType;

import com.hp.hpl.jena.rdf.model.Resource;

public class WorkflowControllerImpl implements WorkflowController {

	private static final Logger LOG = LoggerFactory.getLogger(WorkflowControllerImpl.class);

	protected List<ActivityControllerListener> listeners;
	
	protected Context context;
	
	public WorkflowControllerImpl(Context context){
		this.context = context;
	}
	
	@Override
	public void start() {
		context.getTaskController().addListener(this);
		context.getEventInterfaceManager().addInterfaceListener(this);
	}
	
	@Override
	public void stop() {
		context.getEventInterfaceManager().removeInterfaceListener(this);
		context.getTaskController().removeListener(this);
	}
	
	protected void notifyWorkflowFinished(FlowOntologyModel jobModel, String jobUri, String workflowUri) {
		Workflow wf = jobModel.getWorkflowWithURI(workflowUri);
		if(wf.getActivityOfWorkflow() != null) {
			onActivityFinished(jobUri, workflowUri, null);
		}
		
		if(listeners == null) return;
		for(ActivityControllerListener listener : listeners) {
			listener.onActivityFinished(jobUri, workflowUri, null);
		}
	}
	
	protected void notifyWorkflowStarted(FlowOntologyModel jobModel, String jobUri, String workflowUri) {
		Workflow wf = jobModel.getWorkflowWithURI(workflowUri);
		if(wf.getActivityOfWorkflow() != null) {
			onActivityStarted(jobUri, workflowUri);
		}
		
		if(listeners == null) return;
		for(ActivityControllerListener listener : listeners) {
			listener.onActivityStarted(jobUri, workflowUri);
		}
	}
	
	protected void notifyWorkflowError(FlowOntologyModel jobModel, String jobUri, String workflowUri) {
		Workflow wf = jobModel.getWorkflowWithURI(workflowUri);
		if(wf.getActivityOfWorkflow() != null) {
			onActivityError(jobUri, workflowUri);
		}
		
		if(listeners == null) return;
		for(ActivityControllerListener listener : listeners) {
			listener.onActivityError(jobUri, workflowUri);
		}
	}
	
	protected void notifyWorkflowCanceled(FlowOntologyModel jobModel, String jobUri, String workflowUri) {
		Workflow wf = jobModel.getWorkflowWithURI(workflowUri);
		if(wf.getActivityOfWorkflow() != null) {
			onActivityCancelled(jobUri, workflowUri);
		}
		
		if(listeners == null) return;
		for(ActivityControllerListener listener : listeners) {
			listener.onActivityCancelled(jobUri, workflowUri);
		}
	}
	
//	@Override
//	public void startPending(String jobUri) {
//		TripleStoreConnection store = context.getStoreConnection();
//		store.openStore();
//		
//		FlowOntologyModel jobModel = store.connectNamedModel(jobUri);
//				
//		Iterator<Workflow> wfIt = jobModel.listWorkflows();
//		while(wfIt.hasNext()) {
//			Workflow wf = wfIt.next();
//			if(wf.getActivityOfWorkflow() == null && wf.getActivityOfLoop() == null && wf.getPropertyResourceValue(FLOWO.hasState).getURI().compareTo(FLOWO.inProgress.getURI()) == 0) {
//			
//				Set<Activity> runningActs = new HashSet<Activity>();
//				Set<Activity> lastActs = new HashSet<Activity>();
//				
//				Iterator<Activity> actIt = wf.listHasActivityActivitys();
//				while(actIt.hasNext()) {
//					Activity act = actIt.next();
//					
//					if(act.getPropertyResourceValue(FLOWO.hasState).getURI().compareTo(FLOWO.inProgress.getURI()) == 0)
//						runningActs.add(act);
//					
//					if(!act.listTransitionToActivitys().hasNext())
//						lastActs.add(act);
//				}
//				
//				if(runningActs.size() > 0) {
//					for(Activity act : runningActs) {
//						if(act.isTask()) {
//							context.getTaskController().startPending(jobUri, act.getURI());
//						} else if(act.isEvent()) {
//							LOG.error("Cannot continue from a pending event; not implemented!");
//						} else if(act.isGateway()) {
//							LOG.error("Cannot continue from a pending event; not implemented!");
//						} else if(act.isWorkflow()) {
//							LOG.error("Cannot continue from a pending sub-workflow; not implemented!");
//						}
//					}
//				} else {
//					OntModelHelper ontModelHelper = context.getOntModelHelperManager().get(store, jobUri, wf.getRootWorkflow().getLocalName());
//										
//					for(Activity act : lastActs)
//						if(act.isGateway()) {
//							LOG.error("Cannot continue from a gateway; not implemented!");
//						} else
//							runNextActivities(store, ontModelHelper, jobModel, jobUri, wf, act, null);	
//					
//				}
//			}
//		}
//		
//		store.closeStore();
//	}
		
	@Override
	public void startWorkflow(String jobUri, String workflowUri) {
		
		TripleStoreConnection store = context.getStoreConnection();
		store.openStore();
		
		// the workflow model (contains all workflow data)
		FlowOntologyModel jobModel = store.connectNamedModel(jobUri);
		Workflow wf = jobModel.getWorkflowWithURI(workflowUri);
		
		// just set the state to inProgress
		wf.removeAll(FLOWO.hasState);
		wf.addProperty(FLOWO.hasState, FLOWO.inProgress);

		// run the first activities of the workflow
		List<Activity> actList = wf.listHasFirstActivityActivitys().toList();
		for(Activity act : actList) {
			wf.addHasCurrentActivityActivity(act);
			Runnable startActivity = new WorkflowControllerRunActivityCommand(this, jobUri, act.getURI());
			context.getExecutorService().execute(startActivity);	
		}

		LOG.debug("Started Workflow " + workflowUri);

		notifyWorkflowStarted(jobModel, jobUri, workflowUri);
		
		store.closeStore();
	}
	
	@Override
	public void onActivityStarted(String jobUri, String activityUri) {
		TripleStoreConnection store = context.getStoreConnection();
		store.openStore();
		
		LOG.debug("Started Activity " + activityUri);
		
		// get reference to the workflow model through the engine model
		FlowOntologyModel jobModel = store.connectNamedModel(jobUri);
		Activity act = jobModel.getActivityWithURI(activityUri);
		
		// set dates and state of activity to inProgress
		act.removeAll(FLOWO.hasState);
		act.addProperty(FLOWO.hasState, FLOWO.inProgress);
		act.setHasStartDate(new DateContainer(DateContainerType.DATETIME));
		
		store.closeStore();
	}
	
	@Override
	public void onEvent(String subscriptionId, String jobUri, String eventClassUri, String eventUri, String interfaceUri) {
		TripleStoreConnection store = context.getStoreConnection();
		store.openStore();
		
		FlowOntologyModel jobModel = store.connectNamedModel(jobUri);
		Event ev = jobModel.getEventWithURI(eventUri);
		if(ev == null || !ev.hasLiteral(jobModel.getProperty(EventSubscription.SUBSCRIPTION_PROP), subscriptionId)) {
			store.closeStore();
			return;
		}
		
		LOG.debug("Received Event " + eventUri + " (" + eventClassUri + ") from " + interfaceUri);
				
		onActivityFinished(jobUri, eventUri, null);
		
		store.closeStore();
	}
	
	// Listeners For Gateway
	@Override
	public void onActivityFinished(String jobUri, String activityUri, List<String> nextActivities) {
		TripleStoreConnection store = context.getStoreConnection();
		store.openStore();
		
		FlowOntologyModel jobModel = store.connectNamedModel(jobUri);
		
		Activity act = jobModel.getActivityWithURI(activityUri);
						
		if(act.getPropertyResourceValue(FLOWO.hasState).getURI().compareToIgnoreCase(FLOWO.inProgress.getURI()) != 0) {
			LOG.warn("Activity state is not " + FLOWO.inProgress.getURI() + ". Possibly already finished? Doing nothing...");
			store.closeStore();
			return;
		}
		
		LOG.debug("Finished Activity " + activityUri);
		
		Workflow wf = act.getActivityOfWorkflow();
		act.removeAll(FLOWO.hasState);
		act.addProperty(FLOWO.hasState, FLOWO.finished);
		act.setHasCompletionDate(new DateContainer(DateContainerType.DATETIME));
		wf.removeHasCurrentActivityActivity(act);
	
		if(act.isEvent()) {
			// unsubscribe
			Runnable unsubscribeEvent = new WorkflowControllerUnsubscribeEventCommand(this, jobUri, act.getURI());
			context.getExecutorService().execute(unsubscribeEvent);	
		}
		
		OntModelHelper ontModelHelper = context.getOntModelHelperManager().get(store, jobUri, wf.getRootWorkflow().getLocalName());
		runNextActivities(store, ontModelHelper, jobModel, jobUri, wf, act, nextActivities);
		
		store.closeStore();
	}
	
	private void runNextActivities(TripleStoreConnection store, OntModelHelper ontModelHelper, FlowOntologyModel jobModel, String jobUri, Workflow wf, Activity act, List<String> nextActivities) {
		// if for some reason the workflow is not running (inProgress) do nothing more
		// let the other state change listener methods handle the occurrence
		if(!wf.getPropertyResourceValue(FLOWO.hasState).getURI().equals(FLOWO.inProgress.getURI())) {
			LOG.debug("Workflow " + wf.getURI() + " does not have state inProgress (it is " + wf.getPropertyResourceValue(FLOWO.hasState).getURI() + "). Cannot continue...");
			return;
		}
		
		FlowOntologyModel engineModel = store.connectModel();
		Job job = wf.getIsWorkflowOfJob();
		
		// instantiate and run next activity				
		Resource clazz = null;
				
		if(act.isGateway())	
			clazz = ontModelHelper.getDirectLeafType(act, FLOWO.Gateway.getURI());
		else if(act.isEvent())
			clazz = ontModelHelper.getDirectLeafType(act, FLOWO.Event.getURI());
		else if(act.isTask())
			clazz = ontModelHelper.getDirectLeafType(act, FLOWO.Task.getURI());
		else if(act.isWorkflow())
			clazz = ontModelHelper.getDirectLeafType(act, FLOWO.Workflow.getURI());
				
		// set transitions
		Tree transitionToTree = ontModelHelper.getApplicableEdgeDestinationsClosure(FLOWO.transitionTo.getURI(), clazz.getURI(), FLOWO.Activity.getURI());
		
		// #### LOOP CODE
		if(act.isLoop()) {
			clazz = ontModelHelper.getDirectLeafType(act, FLOWO.Loop.getURI());
			Loop loop = act.as(Loop.class);
			if(loop.getIteration() < (loop.getMaxIterations()-1)) {
				transitionToTree = new Tree();
				transitionToTree.addChildren(clazz.getURI());
			}
		}
		// #### LOOP CODE
		
		if(transitionToTree != null && !transitionToTree.isEmpty()) {
			for(Tree tree : transitionToTree) {
				
				if(nextActivities == null || nextActivities.contains(tree.getNode())) {
					Activity nextInstance = context.getInstantiator().getActivityInstance(ontModelHelper, jobModel, engineModel, ontModelHelper.getFlowModel().getResource(tree.getNode()), wf, job);
					act.addTransitionToActivity(nextInstance);
					nextInstance.addTransitionFromActivity(act);
					
					// #### LOOP CODE
					if(nextInstance.isLoop() && act.isLoop() && clazz.getURI().compareTo(tree.getNode()) == 0) {
						Loop prevLoop = act.as(Loop.class);
						Loop nextLoop = nextInstance.as(Loop.class);
						nextLoop.setIteration(prevLoop.getIteration()+1);
					}
					// #### LOOP CODE
					
					wf.addHasCurrentActivityActivity(nextInstance);
					Runnable startActivity = new WorkflowControllerRunActivityCommand(this, jobUri, nextInstance.getURI());
					context.getExecutorService().execute(startActivity);
					
					if(nextInstance.getHasTimeout() > 0) {
						WorkflowControllerTimeoutActivityCommand timeoutCommand = new WorkflowControllerTimeoutActivityCommand(this, jobUri, nextInstance.getURI());
						context.getScheduledExecutorService().schedule(timeoutCommand, nextInstance.getHasTimeout(), TimeUnit.MILLISECONDS);
					}
				}
			}
		}
	
		Iterator<Activity> activityIt = wf.listHasCurrentActivityActivitys();
		if(!activityIt.hasNext()) notifyWorkflowFinished(jobModel, jobUri, wf.getURI());
	}

	@Override
	public void onActivityError(String jobUri, String activityUri) {
		TripleStoreConnection store = context.getStoreConnection();
		store.openStore();
		
		FlowOntologyModel jobModel = store.connectNamedModel(jobUri);
		
		Activity act = jobModel.getActivityWithURI(activityUri);
		act.removeAll(FLOWO.hasState);
		act.addProperty(FLOWO.hasState, FLOWO.withError);
		act.setHasCompletionDate(new DateContainer(DateContainerType.DATETIME));
		
		if(act.isEvent()) {
			// unsubscribe
			Runnable unsubscribeEvent = new WorkflowControllerUnsubscribeEventCommand(this, jobUri, act.getURI());
			context.getExecutorService().execute(unsubscribeEvent);	
		}
		
		// find workflow instance
		Workflow wf = act.getActivityOfWorkflow();
		wf.removeHasCurrentActivityActivity(act);
		
		// first time error in workflow (parallel activities might trigger onError multiple times)
		if(!wf.getPropertyResourceValue(FLOWO.hasState).getURI().equals(FLOWO.withError.getURI())) {
			wf.removeAll(FLOWO.hasState);
			wf.addProperty(FLOWO.hasState, FLOWO.withError);
			notifyWorkflowError(jobModel, jobUri, wf.getURI());
		}
		
		LOG.debug("Error on Activity " + activityUri);
		
		store.closeStore();
	}

	@Override
	public void onActivityCancelled(String jobUri, String activityUri) {
		TripleStoreConnection store = context.getStoreConnection();
		store.openStore();
		
		FlowOntologyModel jobModel = store.connectNamedModel(jobUri);
		Activity act = jobModel.getActivityWithURI(activityUri);
		
		if(act == null) {
			LOG.warn("Activity " + activityUri + " does not exist! Probably an InstantiationEvent...");
			store.closeStore();
			return;
		}
		
		act.removeAll(FLOWO.hasState);
		act.addProperty(FLOWO.hasState, FLOWO.canceled);
		act.setHasCompletionDate(new DateContainer(DateContainerType.DATETIME));
		
		if(act.isEvent()) {
			// unsubscribe
			Runnable unsubscribeEvent = new WorkflowControllerUnsubscribeEventCommand(this, jobUri, act.getURI());
			context.getExecutorService().execute(unsubscribeEvent);	
		}
		
		// find workflow instance
		Workflow wf = act.getActivityOfWorkflow();
		wf.removeHasCurrentActivityActivity(act);

		// canceling an activity = canceling the whole workflow
		// first time error in workflow (parallel activities might trigger onError multiple times)
		if(!wf.getPropertyResourceValue(FLOWO.hasState).getURI().equals(FLOWO.canceled.getURI())) {
			wf.removeAll(FLOWO.hasState);
			wf.addProperty(FLOWO.hasState, FLOWO.canceled);
			notifyWorkflowCanceled(jobModel, jobUri, wf.getURI());
		}
		
		LOG.debug("Cancelled Activity " + activityUri);
		
		store.closeStore();
	}
	
	@Override
	public void onActivityTimeout(String jobUri, String activityUri) {
		TripleStoreConnection store = context.getStoreConnection();
		store.openStore();
		
		FlowOntologyModel jobModel = store.connectNamedModel(jobUri);
		Activity act = jobModel.getActivityWithURI(activityUri);
		
		if(act == null) {
			LOG.warn("Activity " + activityUri + " does not exist! Probably an InstantiationEvent...");
			store.closeStore();
			return;
		}
		
		act.removeAll(FLOWO.hasState);
		act.addProperty(FLOWO.hasState, FLOWO.timedout);
		act.setHasCompletionDate(new DateContainer(DateContainerType.DATETIME));
		
		if(act.isEvent()) {
			// unsubscribe
			Runnable unsubscribeEvent = new WorkflowControllerUnsubscribeEventCommand(this, jobUri, act.getURI());
			context.getExecutorService().execute(unsubscribeEvent);	
		} else if(act.isTask()) {
			context.getTaskController().cancelTask(jobUri, activityUri, true);
		}
		
		// find workflow instance
		Workflow wf = act.getActivityOfWorkflow();
		wf.removeHasCurrentActivityActivity(act);
		
		LOG.debug("Timedout Activity " + activityUri);
		
		OntModelHelper ontModelHelper = context.getOntModelHelperManager().get(store, jobUri, wf.getRootWorkflow().getLocalName());
		runNextActivities(store, ontModelHelper, jobModel, jobUri, wf, act, null);
		
		store.closeStore();
	}
	
	@Override
	public boolean addListener(ActivityControllerListener l) {
		if (l == null)
			return false;
		if (listeners == null)
			listeners = new ArrayList<ActivityControllerListener>();
		if (!listeners.contains(l))
			listeners.add(l);
		return true;
	}

	@Override
	public boolean removeListener(ActivityControllerListener l) {
		if (listeners == null || l == null)
			return false;
		return listeners.remove(l);
	}
	
	@Override
	public void removeAllListeners() {
		if(listeners == null) return;
		listeners.clear();
	}

	@Override
	public Context getContext() {
		return context;
	}
}
